Skip to content

feat: implement PATCH /players/{squadNumber} for partial updates#329

Open
the-Sunny-Sharma wants to merge 5 commits into
nanotaboada:masterfrom
the-Sunny-Sharma:feat/path-players-318
Open

feat: implement PATCH /players/{squadNumber} for partial updates#329
the-Sunny-Sharma wants to merge 5 commits into
nanotaboada:masterfrom
the-Sunny-Sharma:feat/path-players-318

Conversation

@the-Sunny-Sharma
Copy link
Copy Markdown

@the-Sunny-Sharma the-Sunny-Sharma commented May 9, 2026

Closes #318

What this PR does

Implements PATCH /players/{squadNumber} for partial player updates
following RFC 7396 (JSON Merge Patch) semantics.

Changes

  • Added PlayerPatchDTO — all fields nullable, annotated with
    @JsonInclude(NON_NULL), excludes id and squadNumber from patching;
    JSR-380 Bean Validation constraints (@Size, @Min, @Max, @Past)
    added so validation triggers only when a field is present
  • Added patch() in PlayersService — applies only non-null fields
    to the existing entity, follows existing Optional chain pattern;
    null guard for both squadNumber and playerPatchDTO
  • Added @PatchMapping("/players/{squadNumber}") in PlayersController
    with @Valid, full Swagger @Operation/@ApiResponses documentation,
    and null-body guard before field access
  • Added 4 unit tests for patch() covering success (with explicit patch
    semantics assertion), not found, null squad number, and null payload
  • Updated CHANGELOG.md under [Unreleased]

Acceptance criteria

  • PATCH /players/{squadNumber} implemented
  • Only non-null fields updated; absent fields unchanged
  • Returns 204 No Content on success
  • Returns 400 Bad Request if body contains squadNumber or id
  • Returns 404 Not Found when player not found
  • All 45 tests pass (41 existing + 4 new patch tests)
  • Code style matches existing patterns throughout
  • Bean Validation constraints on PlayerPatchDTO
  • CHANGELOG.md updated under [Unreleased]

This change is Reviewable

Summary by CodeRabbit

Release Notes

  • New Features

    • Added PATCH /players/{squadNumber} endpoint to enable partial updates of player records
    • Player identity fields (id and squadNumber) are immutable and will return 400 Bad Request if included in requests
  • Tests

    • Comprehensive test coverage added for patch operations
  • Documentation

    • Updated changelog with PATCH endpoint implementation details

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Walkthrough

Adds RFC 7396-style PATCH support: a nullable PlayerPatchDTO, PlayersService.patch applying only non-null fields and evicting the players cache, PlayersController.patch validating immutable fields and returning 204/400/404, plus unit tests and a changelog entry.

Changes

PATCH Endpoint for Partial Player Updates

Layer / File(s) Summary
PlayerPatchDTO (data shape)
src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java
New DTO with @Data and @JsonInclude(NON_NULL), nullable patchable fields, Jakarta validation, and Javadoc describing merge-patch semantics and forbidden id/squadNumber.
Service implementation
src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java
Adds patch(Integer, PlayerPatchDTO) with null checks, lookup by squadNumber, apply non-null DTO fields, save, and @CacheEvict("players", allEntries=true) returning boolean.
Controller endpoint & imports
src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java
Adds @PatchMapping("/players/{squadNumber}"), imports, validates request body forbids id/squadNumber, delegates to service, returns 204 No Content on true, 404 Not Found on false, 400 Bad Request for forbidden fields.
Controller PATCH tests
src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java
Adds imports and tests asserting 400 when immutable fields are present, 204 on success, and 404 when player missing; verifies mock interactions.
Service patch tests
src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java
Adds import and six unit tests covering success, missing player, null squadNumber, null payload, partial-field skipping, and full updates.
Changelog
CHANGELOG.md
Unreleased Added entry documenting the new PATCH endpoint, DTO, service behavior, and response codes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement PATCH /players/{squadNumber} endpoint (#318)
All fields except squadNumber and id are patchable; absent fields unchanged (#318)
Returns 204 No Content on success; 400 for forbidden fields; 404 when missing (#318)
Tests and CHANGELOG updated (#318)

Possibly related issues

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format (feat:), is under 80 characters (64 chars), and clearly summarizes the main change: implementing a PATCH endpoint for partial player updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ sync documentation
  • 🛠️ enforce http error handling
  • 🛠️ idiomatic review
  • 🛠️ verify api contract

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java`:
- Around line 250-254: In the patch method, add request validation and a
null-body guard: annotate the PlayerPatchDTO parameter with `@Valid` (e.g., public
ResponseEntity<Void> patch(`@PathVariable` Integer squadNumber, `@Valid`
`@RequestBody` PlayerPatchDTO playerPatchDTO)) and update the initial conditional
to check for null before dereferencing (e.g., if (playerPatchDTO == null ||
playerPatchDTO.getSquadNumber() != null || playerPatchDTO.getId() != null)
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();). Ensure the
controller imports javax.validation.Valid (or jakarta.validation.Valid) so DTO
constraints on PlayerPatchDTO are enforced.

In
`@src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java`:
- Around line 35-45: PlayerPatchDTO lacks Bean Validation annotations—add the
same field-level JSR-380 constraints used in PlayerDTO but using constraints
that permit null (so patch fields remain optional): annotate squadNumber with
`@Min/`@Max, dateOfBirth with `@Past`, string fields (firstName, middleName,
lastName, position, abbrPosition, team, league) with `@Size`(max=...) and any
`@Pattern`(...) used in PlayerDTO for formats, and apply any ID/UUID format
constraint used in PlayerDTO to id; do not add `@NotNull/`@NotBlank (since fields
must remain nullable). Import javax.validation.constraints.* and mirror the
exact size/regex/min-max values from PlayerDTO when updating class
PlayerPatchDTO.

In
`@src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java`:
- Around line 235-243: The method currently dereferences playerPatchDTO without
null-checking (see squadNumber check and the
playersRepository.findBySquadNumber(...).map(existing -> { ... }) block), so add
an early guard for playerPatchDTO == null at the start of the method (similar to
the squadNumber null check) to log a warning and return false; then proceed to
use playerPatchDTO safely in the mapping where existing.setFirstName(...),
existing.setMiddleName(...), etc. are called. Ensure the log uses the same
logger (log) and behavior as the squadNumber guard to keep semantics consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 288c1c36-cc52-413f-a575-628f4f188d18

📥 Commits

Reviewing files that changed from the base of the PR and between db4db58 and 0ea4892.

📒 Files selected for processing (3)
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java (1)

409-410: ⚡ Quick win

Use fake factories for PlayerPatchDTO fixtures in tests.

Please replace direct new PlayerPatchDTO() setup with a test fake factory (e.g., PlayerPatchDTOFakes) to keep fixture creation consistent and reusable.

As per coding guidelines, "Ensure test data uses fake factories (PlayerFakes, PlayerDTOFakes)."

Also applies to: 431-432, 452-452

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java`
around lines 409 - 410, Replace the inline fixture creation using new
PlayerPatchDTO() in PlayersServiceTests with the shared test fake factory (e.g.,
PlayerPatchDTOFakes); instead of constructing and calling
dto.setFirstName("Updated") directly, call the appropriate factory method on
PlayerPatchDTOFakes to produce a DTO prepopulated or then customize it via the
factory (same change for the other occurrences at lines referenced: the second
dto creation and the third occurrence), ensuring tests use the standard
PlayerFakes/PlayerDTOFakes classes for consistent, reusable fixtures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java`:
- Around line 405-420: The test must assert patch semantics by verifying the
saved Player actually has firstName updated and that an unrelated field remains
unchanged: use an ArgumentCaptor<Player> on playersRepositoryMock.save in
PlayersServiceTests.givenExistingPlayer_whenPatch_thenReturnsTrue to capture the
saved entity and assert saved.getFirstName().equals("Updated") and
saved.getLastName() (or saved.getSquadNumber()) equals the original entity's
value; keep the existing verify(playersRepositoryMock).findBySquadNumber(...)
and the then(actual).isTrue() assertion.

---

Nitpick comments:
In
`@src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java`:
- Around line 409-410: Replace the inline fixture creation using new
PlayerPatchDTO() in PlayersServiceTests with the shared test fake factory (e.g.,
PlayerPatchDTOFakes); instead of constructing and calling
dto.setFirstName("Updated") directly, call the appropriate factory method on
PlayerPatchDTOFakes to produce a DTO prepopulated or then customize it via the
factory (same change for the other occurrences at lines referenced: the second
dto creation and the third occurrence), ensuring tests use the standard
PlayerFakes/PlayerDTOFakes classes for consistent, reusable fixtures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b299dd6-a598-4d37-ab83-0a6d2c7365be

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea4892 and 641e792.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java
  • src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java
  • src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java

@the-Sunny-Sharma
Copy link
Copy Markdown
Author

Hi @nanotaboada, addressed all CodeRabbit review comments in the latest commit:

  • Added null guard for playerPatchDTO in PlayersService.patch()
  • Added @Valid annotation to controller @RequestBody
  • Added JSR-380 Bean Validation constraints (@Size, @Min, @Max, @Past) to PlayerPatchDTO
  • Added 4 unit tests for patch() covering success, not found, null squad number, and null payload
  • Updated CHANGELOG.md under [Unreleased]

All 45 tests pass. Ready for review.

@the-Sunny-Sharma
Copy link
Copy Markdown
Author

Hi @nanotaboada, just wanted to follow up on this PR.
All review comments from CodeRabbit have been addressed,
45 tests pass, SonarQube and CodeFactor are green.
Happy to make any further changes if needed. Thank you for your time!

@nanotaboada
Copy link
Copy Markdown
Owner

Hi @the-Sunny-Sharma, thanks for the contribution — the feature looks well-structured and the PR description is thorough!

The verify CI job is failing due to a JaCoCo branch coverage violation:

Rule violated for bundle java.samples.spring.boot:
branches covered ratio is 0.64, but expected minimum is 0.80

The project enforces ≥ 80% branch coverage as a hard gate. The new patch() method and PlayerPatchDTO introduce conditional branches (null checks, field-by-field guards, validation paths) that aren't fully exercised by the four tests added in this PR.

To fix this, please add tests that cover the remaining branches — for example:

  • Patching a player where only some fields are provided (others remain null and are skipped)
  • Each field-level branch in the service patch() method when a field is present vs. absent
  • The controller's null-body guard path (if not already covered)

You can check coverage locally by running:

./mvnw clean test jacoco:report
open target/site/jacoco/index.html

Once the branch coverage is back at or above 80%, the gate will pass. Happy to help if you have any questions!

@sonarqubecloud
Copy link
Copy Markdown

@the-Sunny-Sharma
Copy link
Copy Markdown
Author

Hi @nanotaboada, thanks for the detailed feedback! I've pushed the additional tests to address the JaCoCo branch coverage failure.

Added to PlayersServiceTests:

  • givenExistingPlayer_whenPatchWithPartialFields_thenOnlyNonNullFieldsUpdated — verifies null fields are skipped
  • givenExistingPlayer_whenPatchWithAllFields_thenAllFieldsUpdated — verifies all patchable fields are applied

Added to PlayersControllerTests:

  • givenBodyContainsSquadNumber_whenPatch_thenReturnsBadRequest
  • givenBodyContainsId_whenPatch_thenReturnsBadRequest
  • givenPlayerExists_whenPatch_thenReturnsNoContent
  • givenUnknownPlayer_whenPatch_thenReturnsNotFound

All 51 tests pass and branch coverage is now at 95% (up from 64%), well above the 80% gate. Please let me know if you'd like any further changes!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java (1)

596-716: ⚡ Quick win

Add tests for null body, validation failures, and empty JSON object.

The PR objectives state the controller includes a "null-body guard" and "Bean Validation on PlayerPatchDTO" with JSR-380 constraints. The current PATCH test suite does not exercise:

  1. Null request body — the guard mentioned in the PR should reject this with 400 Bad Request.
  2. Validation failures — if PlayerPatchDTO has constraints like @Size, @Min, @Max, or @Past that apply when fields are present, invalid values should trigger 422 Unprocessable Entity (similar to the PUT test at line 467).
  3. Empty JSON object {} — a valid JSON Merge Patch no-op that should succeed with 204 if the player exists.

Adding these tests will improve branch coverage and ensure the controller's validation and guard logic is properly exercised.

🧪 Suggested test additions

Test 1: Null body

/**
 * Given the request body is null
 * When attempting to patch a player
 * Then response status is 400 Bad Request and service is never called
 */
`@Test`
void givenNullBody_whenPatch_thenReturnsBadRequest()
        throws Exception {
    // Given
    Integer squadNumber = 10;
    MockHttpServletRequestBuilder request = MockMvcRequestBuilders
            .patch(PATH + "/{squadNumber}", squadNumber)
            .contentType(MediaType.APPLICATION_JSON);
    // When
    MockHttpServletResponse response = application
            .perform(request)
            .andReturn()
            .getResponse();
    // Then
    verify(playersServiceMock, never()).patch(anyInt(), any(PlayerPatchDTO.class));
    then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
}

Test 2: Validation failure (example with invalid @Size constraint)

/**
 * Given the request body contains invalid field data (validation fails)
 * When attempting to patch a player
 * Then response status is 422 Unprocessable Entity and service is never called
 */
`@Test`
void givenInvalidPatchData_whenPatch_thenReturnsUnprocessableEntity()
        throws Exception {
    // Given
    Integer squadNumber = 10;
    PlayerPatchDTO dto = new PlayerPatchDTO();
    dto.setFirstName(""); // Assuming `@Size`(min=1) or similar constraint
    String content = objectMapper.writeValueAsString(dto);
    MockHttpServletRequestBuilder request = MockMvcRequestBuilders
            .patch(PATH + "/{squadNumber}", squadNumber)
            .content(content)
            .contentType(MediaType.APPLICATION_JSON);
    // When
    MockHttpServletResponse response = application
            .perform(request)
            .andReturn()
            .getResponse();
    // Then
    verify(playersServiceMock, never()).patch(anyInt(), any(PlayerPatchDTO.class));
    then(response.getStatus()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.value());
}

Test 3: Empty JSON object (no-op)

/**
 * Given the request body is an empty JSON object (no fields to patch)
 * When attempting to patch a player
 * Then response status is 204 No Content (no-op patch succeeds)
 */
`@Test`
void givenEmptyPatchBody_whenPatch_thenReturnsNoContent()
        throws Exception {
    // Given
    Integer squadNumber = 10;
    PlayerPatchDTO dto = new PlayerPatchDTO(); // All fields null
    String content = objectMapper.writeValueAsString(dto);
    Mockito
            .when(playersServiceMock.patch(squadNumber, dto))
            .thenReturn(true);
    MockHttpServletRequestBuilder request = MockMvcRequestBuilders
            .patch(PATH + "/{squadNumber}", squadNumber)
            .content(content)
            .contentType(MediaType.APPLICATION_JSON);
    // When
    MockHttpServletResponse response = application
            .perform(request)
            .andReturn()
            .getResponse();
    // Then
    verify(playersServiceMock, times(1)).patch(anyInt(), any(PlayerPatchDTO.class));
    then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java`
around lines 596 - 716, Add three unit tests in PlayersControllerTests covering
PATCH null body, validation failure, and empty JSON no-op: (1) implement
givenNullBody_whenPatch_thenReturnsBadRequest that sends a PATCH with no content
to PATH+"/{squadNumber}", asserts playersServiceMock.patch is never called and
response status is 400; (2) implement
givenInvalidPatchData_whenPatch_thenReturnsUnprocessableEntity that posts a
PlayerPatchDTO with an invalid field (e.g., empty firstName assuming `@Size`) and
asserts playersServiceMock.patch is never called and response status is 422; (3)
implement givenEmptyPatchBody_whenPatch_thenReturnsNoContent that posts an
all-null PlayerPatchDTO, stub playersServiceMock.patch(squadNumber, dto) to
return true, verify patch called once, and assert response status is 204; use
objectMapper to serialize DTO, MockMvcRequestBuilders.patch to build requests,
and verify/then assertions consistent with existing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java`:
- Around line 596-716: Add three unit tests in PlayersControllerTests covering
PATCH null body, validation failure, and empty JSON no-op: (1) implement
givenNullBody_whenPatch_thenReturnsBadRequest that sends a PATCH with no content
to PATH+"/{squadNumber}", asserts playersServiceMock.patch is never called and
response status is 400; (2) implement
givenInvalidPatchData_whenPatch_thenReturnsUnprocessableEntity that posts a
PlayerPatchDTO with an invalid field (e.g., empty firstName assuming `@Size`) and
asserts playersServiceMock.patch is never called and response status is 422; (3)
implement givenEmptyPatchBody_whenPatch_thenReturnsNoContent that posts an
all-null PlayerPatchDTO, stub playersServiceMock.patch(squadNumber, dto) to
return true, verify patch called once, and assert response status is 204; use
objectMapper to serialize DTO, MockMvcRequestBuilders.patch to build requests,
and verify/then assertions consistent with existing tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66917d1e-94c3-45d7-9bd7-d51d3ebf9829

📥 Commits

Reviewing files that changed from the base of the PR and between 641e792 and 4129c5c.

📒 Files selected for processing (2)
  • src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java
  • src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement PATCH method for partial updates

2 participants